<?php

namespace SLN0002\DAO;

use Home\DAO\OrgDAO;
use Home\DAO\PSIBaseExDAO;

/**
 * 结账 DAO
 *
 * @author PSI
 * @copyright 2015 - present
 * @license GPL v3
 */
class CADAO extends PSIBaseExDAO
{

  /**
   * 返回所有的公司列表
   */
  public function companyList($params)
  {
    $db = $this->db;

    $dao = new OrgDAO($db);
    $data = $dao->getCompanyExList($params);

    $result = [];

    foreach ($data as $v) {
      $id = $v["id"];

      // 查询当前会计期间
      $sql = "select acc_year, acc_month
              from t_acc_current_period
              where company_id = '%s' ";
      $d = $db->query($sql, $id);
      $currentPerido = "";
      $y = 0;
      $m = 0;
      if ($d) {
        $y = $d[0]["acc_year"];
        $m = $d[0]["acc_month"];
        if ($m < 10) {
          $m = "0{$m}";
        }
        $currentPerido = "{$y}-{$m}";
      }

      // 查询关账情况
      $sql = "select acc_year, acc_month, acc_gl_closed,
              acc_detail_closed, period_closed
            from t_acc_period
            where company_id = '%s' and acc_year = %d and acc_month = %d";
      $d = $db->query($sql, $id, $y, $m);
      $mark = "√";
      $glClosed = $d[0]["acc_gl_closed"] == 1 ? $mark : null;
      $detailClosed = $d[0]["acc_detail_closed"] == 1 ? $mark : null;

      $result[] = [
        "id" => $id,
        "code" => $v["code"],
        "name" => $v["name"],
        "orgType" => $v["orgType"],
        "currentPeriod" => $currentPerido,
        "detailClosed" => $detailClosed,
        "glClosed" => $glClosed,
      ];
    }
    return $result;
  }

  /**
   * 执行结账操作
   */
  public function actionCloseAcc(&$params)
  {
    $db = $this->db;

    $companyId = $params["companyId"];

    $sql = "select name
            from t_org where id = '%s' and parent_id is null ";
    $data = $db->query($sql, $companyId);
    if (!$data) {
      return $this->badParam("companyId");
    }

    $companyName = $data[0]["name"];
    $daoComm = new AccCommDAO($db);
    $r = $daoComm->getCurrentAccYearAndMonth($companyId);
    if (!$r) {
      return $this->bad("{$companyName}没有建账");
    }
    $accYear = $r["accYear"];
    $accMonth = $r["accMonth"];
    $p = $daoComm->buildPeriodStr($accYear, $accMonth);

    // 检查明细分类账和总分类账是否都已经关账
    $sql = "select acc_gl_closed, acc_detail_closed, period_closed
            from t_acc_period
            where company_id = '%s' and acc_year = %d and acc_month = %d ";
    $data = $db->query($sql, $companyId, $accYear, $accMonth);
    if (!$data) {
      // 莫名的bug
      return $this->bad("{$companyName}没有初始化会计期间");
    }
    $v = $data[0];
    $glClosed = $v["acc_gl_closed"] == 1;
    $detailClosed = $v["acc_detail_closed"] == 1;
    $periodClosed = $v["period_closed"] == 1;
    if ($periodClosed) {
      // 莫名的bug
      return $this->bad("{$companyName}的会计期间[{$p}]已经结账了");
    }
    if (!$detailClosed) {
      return $this->bad("{$companyName}的会计期间[{$p}]的明细分类账还没有关账");
    }
    if (!$glClosed) {
      return $this->bad("{$companyName}的会计期间[{$p}]的总分类账还没有关账");
    }

    // 检查损益类账户不能有余额
    $listSubject = [
      "6001",
      "6051",
      "6111",
      "6301",
      "6401",
      "6402",
      "6403",
      "6601",
      "6602",
      "6603",
      "6701",
      "6711",
      "6701"
    ];
    foreach ($listSubject as $subjectCode) {
      $sql = "select id, db_table_name_prefix, subject_code
              from t_acc_fmt
              where company_id = '%s' and left(subject_code, 4) = '%s' 
                and in_use = 1";
      $data = $db->query($sql, $companyId, $subjectCode);
      foreach ($data as $v) {
        $code = $v["subject_code"];
        $tableName = $v["db_table_name_prefix"] . "_detail";
        $sql = "select acc_balance from {$tableName}
                where company_id = '%s' and voucher_year = %d and voucher_month = %d
                order by id desc limit 1";
        $dataAcc = $db->query($sql, $companyId, $accYear, $accMonth);
        if ($dataAcc) {
          $balance = $dataAcc[0]["acc_balance"];
          if (abs($balance) > 0.0000001) {
            return $this->bad("科目{$code}还有余额，需要先做结转损益");
          }
        }
      }
    }

    // 关闭当前会计期间：做期间结账标志
    $sql = "update t_acc_period
            set period_closed = 1
            where company_id = '%s' and acc_year = %d and acc_month = %d";
    $rc = $db->execute($sql, $companyId, $accYear, $accMonth);
    if ($rc === false) {
      return $this->sqlError(__METHOD__, __LINE__);
    }

    // 年末结转 - 如果当前会计期间是年末，则需要：做年末结转标志和初始化下一年的会计期间
    if ($accMonth == 12) {
      $sql = "update t_acc_period
              set year_forward = 1
              where company_id = '%s' and acc_year = %d and acc_month = %d";
      $rc = $db->execute($sql, $companyId, $accYear, $accMonth);
      if ($rc === false) {
        return $this->sqlError(__METHOD__, __LINE__);
      }

      // 初始化下一年的会计期间
      $y = $accYear + 1;
      for ($m = 1; $m < 13; $m++) {
        $sql = "insert into t_acc_period(id, company_id, acc_year, acc_month,
                  acc_gl_kept, acc_gl_closed, acc_detail_kept, acc_detail_closed,
                  period_closed, year_forward)
                values ('%s', '%s', %d, %d,
                  0, 0, 0, 0, 
                  0, 0)";
        $rc = $db->execute($sql, $this->newId(), $companyId, $y, $m);
        if ($rc === false) {
          return $this->sqlError(__METHOD__, __LINE__);
        }
      }
    }

    $lastAccYear = $accYear;
    $lastAccMonth = $accMonth;
    $lastDay = $daoComm->buildVoucherLastDT($lastAccYear, $lastAccMonth);

    // 把当前会计期间调整到下一个会计期间
    if ($accMonth == 12) {
      $accYear = $accYear + 1;
      $accMonth = 1;
    } else {
      $accMonth = $accMonth + 1;
    }
    $sql = "update t_acc_current_period
            set acc_year = %d, acc_month = %d
            where company_id = '%s' ";
    $rc = $db->execute($sql, $accYear, $accMonth, $companyId);
    if ($rc === false) {
      return $this->sqlError(__METHOD__, __LINE__);
    }
    $firstDay = $daoComm->buildPeriodStr($accYear, $accMonth) . "-01";

    // 明细账上月期末数据 、明细账本月期初数据
    $sql = "select id, db_table_name_prefix
            from t_acc_fmt
            where company_id = '%s' ";
    $data = $db->query($sql, $companyId);
    foreach ($data as $v) {
      $fmtId = $v["id"];
      $tableName = $v["db_table_name_prefix"] . "_detail";

      // 自定义账样列
      $sql = "select db_field_name, voucher_input
              from t_acc_fmt_cols
              where fmt_id = '%s' and sys_col != 1 and col_category != 1
              order by show_order";
      $dataFmtCols = $db->query($sql, $fmtId);

      // 子账簿个数
      $sql = "select count(*) as cnt
              from t_acc_fmt_cols
              where fmt_id = '%s' and sub_acc_level >0";
      $dataSubAcc = $db->query($sql, $fmtId);
      $cnt = $dataSubAcc[0]["cnt"];
      $subAccLevel = -1;
      if ($cnt > 0) {
        $subAccLevel = $cnt;
      }

      $itemExFields = [];
      $cpmFieldList = ["x_db_cnt", "x_db_price", "x_cr_cnt", "x_cr_price"];
      $cpm = [];
      foreach ($dataFmtCols as $col) {
        $fieldName = $col["db_field_name"];
        $vi = $col["voucher_input"];

        if (in_array($fieldName, $cpmFieldList)) {
          $cpm[] = $fieldName;
        }

        $itemExFields[] = $fieldName;
        if ($vi == 2) {
          // 码表录入
          $itemExFields[] = "{$fieldName}_id";
          $itemExFields[] = "{$fieldName}_name";
        }
      }
      $isCPM = count($cpm) == 4; // 数量单价金额三栏账

      $sqlForInsert = "insert into {$tableName} (
                        acc_id, company_id, sub_acc_level, acc_flag, 
                        acc_user_id, biz_user_id, subject_code, voucher_dt, 
                        voucher_year, voucher_month, voucher_word, voucher_number,
                        je_number, acc_user_name, biz_user_name, acc_db, acc_cr, 
                        acc_balance, acc_balance_dbcr, voucher_summary";
      if (count($itemExFields) > 0) {
        $sqlForInsert .= ", ";
        $sqlForInsert .= implode(",", $itemExFields);
      }
      $sqlForInsert .= ") 
                        select '%s' as acc_id, company_id, sub_acc_level, %d as acc_flag, 
                          '' as acc_user_id, '' as biz_user_id, subject_code, '%s' as voucher_dt, 
                          %d as voucher_year, %d as voucher_month, '' as voucher_word, null as voucher_number,
                          null as je_number, '' as acc_user_name, '' as biz_user_name, null as acc_db, null as acc_cr, 
                          acc_balance, acc_balance_dbcr, '%s' as voucher_summary";
      if (count($itemExFields) > 0) {
        $sqlForInsert .= ",";
        $sqlForInsert .= implode(",", $itemExFields);
      }
      $sqlForInsert .= " from {$tableName} 
                          where sub_acc_level = %d and acc_flag = 1500 
                          order by id desc 
                          limit 1";

      $sqlForSumInsert = "insert into {$tableName} (
                            acc_id, company_id, sub_acc_level, acc_flag, 
                            acc_user_id, biz_user_id, subject_code, voucher_dt, 
                            voucher_year, voucher_month, voucher_word, voucher_number,
                            je_number, acc_user_name, biz_user_name, acc_db, acc_cr, 
                            acc_balance, acc_balance_dbcr, voucher_summary";
      if (count($itemExFields) > 0) {
        $sqlForSumInsert .= ", ";
        $sqlForSumInsert .= implode(",", $itemExFields);
      }
      $sqlForSumInsert .= ") 
                        select '%s' as acc_id, company_id, sub_acc_level, %d as acc_flag, 
                          '' as acc_user_id, '' as biz_user_id, subject_code, '%s' as voucher_dt, 
                          %d as voucher_year, %d as voucher_month, '' as voucher_word, null as voucher_number,
                          null as je_number, '' as acc_user_name, '' as biz_user_name, %f as acc_db, %f as acc_cr, 
                          acc_balance, acc_balance_dbcr, '%s' as voucher_summary";
      if (count($itemExFields) > 0) {
        $sqlForSumInsert .= ",";
        $sqlForSumInsert .= implode(",", $itemExFields);
      }
      $sqlForSumInsert .= " from {$tableName} 
                          where sub_acc_level = %d and acc_flag = 1500 
                          order by id desc 
                          limit 1";

      $listSubAccLevel = [-1];
      if ($subAccLevel > 0) {
        for ($i = 1; $i <= $subAccLevel; $i++) {
          $listSubAccLevel[] = $i;
        }
      }

      foreach ($listSubAccLevel as $sal) {
        // 明细账上月期末数据
        $accId = $this->newId();
        $rc = $db->execute($sqlForInsert, $accId, 2000, $lastDay, $lastAccYear, $lastAccMonth, '本月期末', $sal);
        if ($rc === false) {
          return $this->sqlError(__METHOD__, __LINE__);
        }
        if ($isCPM) {
          // 补丁
          $sql = "update {$tableName}
                  set x_db_cnt = null, x_db_price = null, x_cr_cnt = null, x_cr_price = null
                  where acc_id = '%s' ";
          $rc = $db->execute($sql, $accId);
          if ($rc === false) {
            return $this->sqlError(__METHOD__, __LINE__);
          }
        }

        // 明细账上月 - 本月合计
        $sql = "select sum(acc_db) as acc_db, sum(acc_cr) as acc_cr
                from {$tableName}
                where voucher_year = %d and voucher_month = %d and acc_flag = 1500 and sub_acc_level = %d";
        $data = $db->query($sql, $lastAccYear, $lastAccMonth, $sal);
        $sumDb = $data[0]["acc_db"];
        $sumCr = $data[0]["acc_cr"];
        $accId = $this->newId();
        $rc = $db->execute(
          $sqlForSumInsert,
          $accId,
          2001,
          $lastDay,
          $lastAccYear,
          $lastAccMonth,
          $sumDb,
          $sumCr,
          '本月合计',
          $sal
        );
        if ($rc === false) {
          return $this->sqlError(__METHOD__, __LINE__);
        }

        if ($isCPM) {
          $sql = "select sum(x_db_cnt) as db_cnt, sum(x_cr_cnt) as cr_cnt
                  from {$tableName}
                  where voucher_year = %d and voucher_month = %d and acc_flag = 1500 and sub_acc_level = %d";
          $data = $db->query($sql, $lastAccYear, $lastAccMonth, $sal);
          $sumDbCnt = floatval($data[0]["db_cnt"]);
          $sumCrCnt = floatval($data[0]["cr_cnt"]);
          $sumDbPrice = 0;
          $sumCrPrice = 0;
          if (abs($sumDbCnt) > 0.000000001) {
            $sumDbPrice = $sumDb / $sumDbCnt;
          }
          if (abs($sumCrCnt) > 0.000000001) {
            $sumCrPrice = $sumCr / $sumCrCnt;
          }

          $sql = "update {$tableName}
                  set x_db_cnt = if(abs(%f)<0.000000001,null,%f), 
                      x_db_price = if(abs(%f)<0.000000001,null,%f),
                      x_cr_cnt = if(abs(%f)<0.000000001,null,%f), 
                      x_cr_price = if(abs(%f)<0.000000001,null,%f)
                  where acc_id = '%s' ";
          $rc = $db->execute(
            $sql,
            $sumDbCnt,
            $sumDbCnt,
            $sumDbPrice,
            $sumDbPrice,
            $sumCrCnt,
            $sumCrCnt,
            $sumCrPrice,
            $sumCrPrice,
            $accId
          );
          if ($rc === false) {
            return $this->sqlError(__METHOD__, __LINE__);
          }
        }

        // 明细账上月 - 本年累计
        $sql = "select sum(acc_db) as acc_db, sum(acc_cr) as acc_cr
                from {$tableName}
                where voucher_year = %d and voucher_month <= %d and acc_flag = 1500 and sub_acc_level = %d";
        $data = $db->query($sql, $lastAccYear, $lastAccMonth, $sal);
        $sumDb = $data[0]["acc_db"];
        $sumCr = $data[0]["acc_cr"];
        $accId = $this->newId();
        $rc = $db->execute(
          $sqlForSumInsert,
          $accId,
          2002,
          $lastDay,
          $lastAccYear,
          $lastAccMonth,
          $sumDb,
          $sumCr,
          '本年累计',
          $sal
        );
        if ($rc === false) {
          return $this->sqlError(__METHOD__, __LINE__);
        }

        if ($isCPM) {
          $sql = "select sum(x_db_cnt) as db_cnt, sum(x_cr_cnt) as cr_cnt
                  from {$tableName}
                  where voucher_year = %d and voucher_month <= %d and acc_flag = 1500 and sub_acc_level = %d";
          $data = $db->query($sql, $lastAccYear, $lastAccMonth, $sal);
          $sumDbCnt = floatval($data[0]["db_cnt"]);
          $sumCrCnt = floatval($data[0]["cr_cnt"]);
          $sumDbPrice = 0;
          $sumCrPrice = 0;
          if (abs($sumDbCnt) > 0.000000001) {
            $sumDbPrice = $sumDb / $sumDbCnt;
          }
          if (abs($sumCrCnt) > 0.000000001) {
            $sumCrPrice = $sumCr / $sumCrCnt;
          }

          $sql = "update {$tableName}
                  set x_db_cnt = if(abs(%f)<0.000000001,null,%f), 
                    x_db_price = if(abs(%f)<0.000000001,null,%f),
                    x_cr_cnt = if(abs(%f)<0.000000001,null,%f), 
                    x_cr_price = if(abs(%f)<0.000000001,null,%f)
                  where acc_id = '%s' ";
          $rc = $db->execute(
            $sql,
            $sumDbCnt,
            $sumDbCnt,
            $sumDbPrice,
            $sumDbPrice,
            $sumCrCnt,
            $sumCrCnt,
            $sumCrPrice,
            $sumCrPrice,
            $accId
          );
          if ($rc === false) {
            return $this->sqlError(__METHOD__, __LINE__);
          }
        }

        // 明细账本月期初数据
        $accId = $this->newId();
        $rc = $db->execute($sqlForInsert, $accId, 1000, $firstDay, $accYear, $accMonth, '本月期初 ', $sal);
        if ($rc === false) {
          return $this->sqlError(__METHOD__, __LINE__);
        }
        if ($isCPM) {
          // 补丁
          $sql = "update {$tableName}
                  set x_db_cnt = null, x_db_price = null, x_cr_cnt = null, x_cr_price = null
                  where acc_id = '%s' ";
          $rc = $db->execute($sql, $accId);
          if ($rc === false) {
            return $this->sqlError(__METHOD__, __LINE__);
          }
        }
      }
    }

    // 总账上月期末数据、总账本月期初数据
    $sql = "select gl_table_name
            from t_subject
            where company_id = '%s' and parent_id is null
              and inited = 1
            order by code";
    $data = $db->query($sql, $companyId);
    foreach ($data as $v) {
      $tableName = $v["gl_table_name"];
      $sqlForInsert = "insert into {$tableName}
                        (acc_id, company_id, acc_user_id, biz_user_id,
                        subject_code, voucher_dt, voucher_year, 
                        voucher_month, acc_user_name, biz_user_name,
                        acc_db, acc_cr, acc_balance, acc_balance_dbcr, voucher_summary, acc_flag)
                      select '%s' as acc_id, company_id, '' as acc_user_id, '' as biz_user_id,
                          subject_code, '%s' as voucher_dt, %d as voucher_year,
                          %d as voucher_month, '' as acc_user_name, '' as biz_user_name,
                          null as acc_db, null as acc_cr, acc_balance, acc_balance_dbcr, '%s' as voucher_summary,
                          %d as acc_flag 
                        from {$tableName}
                        where acc_flag = 1500
                        order by id desc
                        limit 1";
      // 总账上月 - 期末数据
      $rc = $db->execute($sqlForInsert, $this->newId(), $lastDay, $lastAccYear, $lastAccMonth, '本月期末', 2000);
      if ($rc === false) {
        return $this->sqlError(__METHOD__, __LINE__);
      }

      // 总账上月 - 本月合计
      $sql = "select sum(acc_db) as acc_db, sum(acc_cr) as acc_cr
              from {$tableName}
              where acc_flag = 1500 and voucher_year = %d and voucher_month = %d";
      $data = $db->query($sql, $lastAccYear, $lastAccMonth);
      $accDB = $data[0]["acc_db"];
      $accCr = $data[0]["acc_cr"];
      $sqlForInsert = "insert into {$tableName}
                        (acc_id, company_id, acc_user_id, biz_user_id,
                        subject_code, voucher_dt, voucher_year, 
                        voucher_month, acc_user_name, biz_user_name,
                        acc_db, acc_cr, acc_balance, acc_balance_dbcr, voucher_summary, acc_flag)
                      select '%s' as acc_id, company_id, '' as acc_user_id, '' as biz_user_id,
                          subject_code, '%s' as voucher_dt, %d as voucher_year,
                          %d as voucher_month, '' as acc_user_name, '' as biz_user_name,
                          %f as acc_db, %f as acc_cr, acc_balance, acc_balance_dbcr, '%s' as voucher_summary,
                          %d as acc_flag 
                        from {$tableName}
                        where acc_flag = 1500
                        order by id desc
                        limit 1";
      $rc = $db->execute(
        $sqlForInsert,
        $this->newId(),
        $lastDay,
        $lastAccYear,
        $lastAccMonth,
        $accDB,
        $accCr,
        '本月合计',
        2001
      );
      if ($rc === false) {
        return $this->sqlError(__METHOD__, __LINE__);
      }

      // 总账上月 - 本年累计
      $sql = "select sum(acc_db) as acc_db, sum(acc_cr) as acc_cr
              from {$tableName}
              where acc_flag = 1500 and voucher_year = %d and voucher_month <= %d";
      $data = $db->query($sql, $lastAccYear, $lastAccMonth);
      $accDB = $data[0]["acc_db"];
      $accCr = $data[0]["acc_cr"];
      $sqlForInsert = "insert into {$tableName}
                        (acc_id, company_id, acc_user_id, biz_user_id,
                        subject_code, voucher_dt, voucher_year, 
                        voucher_month, acc_user_name, biz_user_name,
                        acc_db, acc_cr, acc_balance, acc_balance_dbcr, voucher_summary, acc_flag)
                      select '%s' as acc_id, company_id, '' as acc_user_id, '' as biz_user_id,
                          subject_code, '%s' as voucher_dt, %d as voucher_year,
                          %d as voucher_month, '' as acc_user_name, '' as biz_user_name,
                          %f as acc_db, %f as acc_cr, acc_balance, acc_balance_dbcr, '%s' as voucher_summary,
                          %d as acc_flag 
                        from {$tableName}
                        where acc_flag = 1500
                        order by id desc
                        limit 1";
      $rc = $db->execute(
        $sqlForInsert,
        $this->newId(),
        $lastDay,
        $lastAccYear,
        $lastAccMonth,
        $accDB,
        $accCr,
        '本年累计',
        2002
      );
      if ($rc === false) {
        return $this->sqlError(__METHOD__, __LINE__);
      }

      // 总账本月期初数据
      $sqlForInsert = "insert into {$tableName}
                        (acc_id, company_id, acc_user_id, biz_user_id,
                        subject_code, voucher_dt, voucher_year, 
                        voucher_month, acc_user_name, biz_user_name,
                        acc_db, acc_cr, acc_balance, acc_balance_dbcr, voucher_summary, acc_flag)
                      select '%s' as acc_id, company_id, '' as acc_user_id, '' as biz_user_id,
                          subject_code, '%s' as voucher_dt, %d as voucher_year,
                          %d as voucher_month, '' as acc_user_name, '' as biz_user_name,
                          null as acc_db, null as acc_cr, acc_balance, acc_balance_dbcr, '%s' as voucher_summary,
                          %d as acc_flag 
                        from {$tableName}
                        where acc_flag = 1500
                        order by id desc
                        limit 1";
      $rc = $db->execute($sqlForInsert, $this->newId(), $firstDay, $accYear, $accMonth, '本月期初', 1000);
      if ($rc === false) {
        return $this->sqlError(__METHOD__, __LINE__);
      }
    }

    // 业务日志
    $log = "[$companyName] - 会计期间[{$p}]结账完毕";
    $params["log"] = $log;

    // 操作成功
    return null;
  }

  /**
   * 结转损益
   */
  public function actionLossAndProfit(&$params)
  {
    $db = $this->db;

    $companyId = $params["companyId"];
    $loginUserId = $params["loginUserId"];
    $loginUserName = $params["loginUserName"];
    $dataOrg = $params["dataOrg"];

    $sql = "select name
            from t_org where id = '%s' and parent_id is null ";
    $data = $db->query($sql, $companyId);
    if (!$data) {
      return $this->badParam("companyId");
    }

    $companyName = $data[0]["name"];
    $daoComm = new AccCommDAO($db);
    $r = $daoComm->getCurrentAccYearAndMonth($companyId);
    if (!$r) {
      return $this->bad("{$companyName}没有建账");
    }
    $accYear = $r["accYear"];
    $accMonth = $r["accMonth"];
    $p = $daoComm->buildPeriodStr($accYear, $accMonth);
    $voucherDT = $daoComm->buildVoucherLastDT($accYear, $accMonth);

    // 检查明细分类账和总分类账是否都已经关账
    $sql = "select acc_gl_closed, acc_detail_closed, period_closed
            from t_acc_period
            where company_id = '%s' and acc_year = %d and acc_month = %d ";
    $data = $db->query($sql, $companyId, $accYear, $accMonth);
    if (!$data) {
      // 莫名的bug
      return $this->bad("{$companyName}没有初始化会计期间");
    }
    $v = $data[0];
    $glClosed = $v["acc_gl_closed"] == 1;
    $detailClosed = $v["acc_detail_closed"] == 1;
    $periodClosed = $v["period_closed"] == 1;
    if ($periodClosed) {
      // 莫名的bug
      return $this->bad("{$companyName}的会计期间[{$p}]已经结账了");
    }
    if (!$detailClosed) {
      return $this->bad("{$companyName}的会计期间[{$p}]的明细分类账还没有关账");
    }
    if (!$glClosed) {
      return $this->bad("{$companyName}的会计期间[{$p}]的总分类账还没有关账");
    }

    // 算法：
    // 将所有损益类科目的余额转入本年利润科目
    // 1. 借方记收入类科目，贷方记本年利润
    // 2. 借方记本年利润，贷方记费用类科目
    // 本年利润，余额在贷方，表示本年的净利润
    // 本年利润，余额在借方，表示本年的净亏损

    // 检查本年利润科目的账样设置
    $subjectCode = "4103"; //  4103 - 本年利润
    $sql = "select id, db_table_name_prefix
            from t_acc_fmt
            where company_id = '%s' and subject_code = '%s'";
    $data = $db->query($sql, $companyId, $subjectCode);
    if (!$data) {
      return $this->bad("科目[4103 - 本年利润]没有初始化账样");
    }
    $v = $data[0];
    $fmtId = $v["id"];

    // 本年利润账样不能有子账簿
    $sql = "select count(*) as cnt
            from t_acc_fmt_cols
            where fmt_id = '%s' and sub_acc_level > 0";
    $data = $db->query($sql, $fmtId);
    $cnt = $data[0]["cnt"];
    if ($cnt > 0) {
      return $this->bad("科目[4103 - 本年利润]不能有子账簿");
    }

    // 此时的当前会计期间的明细分类账和总分类账的关账标记都已经打上了
    // 为了复用之前的代码，临时把关账标记先取消掉，在后面再打上关账标记
    $sql = "update t_acc_period
            set acc_gl_closed = 0, acc_detail_closed = 0
            where company_id = '%s' and acc_year = %d and acc_month = %d";
    $rc = $db->execute($sql, $companyId, $accYear, $accMonth);
    if ($rc === false) {
      return $this->sqlError(__METHOD__, __LINE__);
    }

    $voucherDAO = new VoucherDAO($db);

    // 不使用凭证字
    $wordId = -1;

    $vb = new VoucherBuilder();
    $bill = $vb->buildVoucherHeader($loginUserId, $dataOrg, $companyId, $wordId, $voucherDT, "", "", "", 0);

    $sumDb = 0;
    $sumCr = 0;

    // 收入类
    $listSubject = ["6001", "6051", "6111", "6301"];
    foreach ($listSubject as $subjectCode) {
      $sql = "select id, db_table_name_prefix, subject_code
              from t_acc_fmt
              where company_id = '%s' and left(subject_code, 4) = '%s' 
                and in_use = 1";
      $data = $db->query($sql, $companyId, $subjectCode);
      foreach ($data as $v) {
        $fmtId = $v["id"];
        $tableName = $v["db_table_name_prefix"] . "_detail";
        $codeSub = $v["subject_code"];

        $subAccLevel = -1;
        $sql = "select count(*) as cnt
                from t_acc_fmt_cols
                where fmt_id = '%s' and  sub_acc_level > 0 ";
        $dataTemp = $db->query($sql, $fmtId);
        $cnt = $dataTemp[0]["cnt"];
        if ($cnt > 4) {
          return $this->bad("科目{$codeSub}子账簿超过3个了");
        }
        if ($cnt > 0 && $cnt < 4) {
          $subAccLevel = $cnt;
        }

        $sql = "select id, db_field_name, voucher_input, sys_col
                from t_acc_fmt_cols
                where fmt_id = '%s' 
                order by show_order";
        $dataCols = $db->query($sql, $fmtId);
        $colNameList = [];
        $fmtExCols = []; // 用户自定义列
        foreach ($dataCols as $c) {
          $colName = $c["db_field_name"];
          $voucherInput = $c["voucher_input"];
          $sysCol = $c["sys_col"] == 1;

          $colNameList[] = $colName;
          if ($voucherInput == 2) {
            // 码表录入
            $colNameList[] = "{$colName}_id";
            $colNameList[] = "{$colName}_name";
          }
          if (!$sysCol) {
            $fmtExCols[] = [
              "id" => $c["id"],
              "fieldName" => $colName,
              "voucherInput" => $voucherInput,
            ];
          }
        }

        $colNameStr = implode(",", $colNameList);
        $sql = "select {$colNameStr} from {$tableName}
                where company_id = '%s' and sub_acc_level = %d
                  and voucher_year = %d and voucher_month = %d
                  and subject_code = '%s' 
                order by id desc
                limit 1";
        $dataAcc = $db->query($sql, $companyId, $subAccLevel, $accYear, $accMonth, $codeSub);
        if (!$dataAcc) {
          // 明细账里面没有数据，无需生成结转凭证
          continue;
        }

        $recrodAcc = $dataAcc[0];

        $balance = $recrodAcc["acc_balance"];

        $sumDb += $balance;

        // 借方分录
        $item = $vb->buildVoucherDetailItem($codeSub, $balance, "db", "结转损益 - 自动生成分录");
        $itemEx = [];
        foreach ($fmtExCols as $col) {
          $colId = $col["id"];
          $fieldName = $col["fieldName"];
          $vi = $col["voucherInput"];

          $value = $recrodAcc[$fieldName];
          $codeId = "";
          $codeValue = "";
          $nameValue = "";
          if ($vi == 2) {
            // 码表录入
            $codeValue = $value;
            $codeId = $recrodAcc["{$fieldName}_id"];
            $nameValue = $recrodAcc["{$fieldName}_name"];
          }

          $itemEx[] = $vb->buildVoucherDetailItemEx($colId, $value, $codeId, $codeValue, $nameValue);
        }
        $vb->combineItemAndItemEx($item, $itemEx);
        $vb->addItemToVoucher($bill, $item);
      }
    }

    // 费用类
    $listSubject = ["6401", "6402", "6403", "6601", "6602", "6603", "6701", "6711", "6801"];
    foreach ($listSubject as $subjectCode) {
      $sql = "select id, db_table_name_prefix, subject_code
              from t_acc_fmt
              where company_id = '%s' and left(subject_code, 4) = '%s' 
                and in_use = 1";
      $data = $db->query($sql, $companyId, $subjectCode);
      foreach ($data as $v) {
        $fmtId = $v["id"];
        $tableName = $v["db_table_name_prefix"] . "_detail";
        $codeSub = $v["subject_code"];

        $subAccLevel = -1;
        $sql = "select count(*) as cnt
                from t_acc_fmt_cols
                where fmt_id = '%s' and  sub_acc_level > 0 ";
        $dataTemp = $db->query($sql, $fmtId);
        $cnt = $dataTemp[0]["cnt"];
        if ($cnt > 4) {
          return $this->bad("科目{$codeSub}子账簿超过3个了");
        }
        if ($cnt > 0 && $cnt < 4) {
          $subAccLevel = $cnt;
        }

        $sql = "select id, db_field_name, voucher_input, sys_col
                from t_acc_fmt_cols
                where fmt_id = '%s' 
                order by show_order";
        $dataCols = $db->query($sql, $fmtId);
        $colNameList = [];
        $fmtExCols = []; // 用户自定义列
        foreach ($dataCols as $c) {
          $colName = $c["db_field_name"];
          $voucherInput = $c["voucher_input"];
          $sysCol = $c["sys_col"] == 1;

          $colNameList[] = $colName;
          if ($voucherInput == 2) {
            // 码表录入
            $colNameList[] = "{$colName}_id";
            $colNameList[] = "{$colName}_name";
          }
          if (!$sysCol) {
            $fmtExCols[] = [
              "id" => $c["id"],
              "fieldName" => $colName,
              "voucherInput" => $voucherInput,
            ];
          }
        }

        $colNameStr = implode(",", $colNameList);
        $sql = "select {$colNameStr} from {$tableName}
                where company_id = '%s' and sub_acc_level = %d
                  and voucher_year = %d and voucher_month = %d
                  and subject_code = '%s' 
                order by id desc
                limit 1";
        $dataAcc = $db->query($sql, $companyId, $subAccLevel, $accYear, $accMonth, $codeSub);
        if (!$dataAcc) {
          // 明细账里面没有数据，无需生成结转凭证
          continue;
        }

        $recrodAcc = $dataAcc[0];

        $balance = $recrodAcc["acc_balance"];
        $sumCr += $balance;

        // 贷方分录
        $item = $vb->buildVoucherDetailItem($codeSub, $balance, "cr", "结转损益 - 自动生成分录");
        $itemEx = [];
        foreach ($fmtExCols as $col) {
          $colId = $col["id"];
          $fieldName = $col["fieldName"];
          $vi = $col["voucherInput"];

          $value = $recrodAcc[$fieldName];
          $codeId = "";
          $codeValue = "";
          $nameValue = "";
          if ($vi == 2) {
            // 码表录入
            $codeValue = $value;
            $codeId = $recrodAcc["{$fieldName}_id"];
            $nameValue = $recrodAcc["{$fieldName}_name"];
          }

          $itemEx[] = $vb->buildVoucherDetailItemEx($colId, $value, $codeId, $codeValue, $nameValue);
        }
        $vb->combineItemAndItemEx($item, $itemEx);
        $vb->addItemToVoucher($bill, $item);
      }
    }

    // 4103 - 本年利润，分录
    $m4103 = $sumDb - $sumCr;
    $dir = "";
    if ($m4103 > 0) {
      $dir = "cr";
    } else {
      $m4103 = -$m4103;
      $dir = "db";
    }
    $item = $vb->buildVoucherDetailItem("4103", $m4103, $dir, "结转损益 - 自动生成分录");
    $itemEx = [];
    $vb->combineItemAndItemEx($item, $itemEx);
    $vb->addItemToVoucher($bill, $item);

    // 保存凭证
    $rc = $voucherDAO->addVoucher($bill);
    if ($rc) {
      return $rc;
    }
    $id = $bill["id"];

    // 复核凭证
    $vp = [
      "id" => $id,
      "loginUserId" => $loginUserId,
      "loginUserName" => $loginUserName,
    ];
    $rc = $voucherDAO->commitVoucher($vp);
    if ($rc) {
      return $rc;
    }

    $keepDAO = new KaDAO($db);

    // 自动记明细账
    $keepPararms = [
      "companyId" => $companyId,
      "loginUserId" => $loginUserId,
      "ignoreCheck" => true,
    ];
    $rc = $keepDAO->keepDetail($keepPararms);
    if ($rc) {
      return $rc;
    }

    // 明细账关账
    $sql = "update t_acc_period
            set acc_detail_closed = 1
            where company_id = '%s' and acc_year = %d and acc_month = %d";
    $rc = $db->execute($sql, $companyId, $accYear, $accMonth);
    if ($rc === false) {
      return $this->sqlError(__METHOD__, __LINE__);
    }

    // 自动记总账
    $keepPararms = [
      "companyId" => $companyId,
      "loginUserId" => $loginUserId,
      "summaryExt" => "月底结账系统自动生成",
    ];
    $rc = $keepDAO->keepGl($keepPararms);
    if ($rc) {
      return $rc;
    }

    // 总账关账
    $sql = "update t_acc_period
            set acc_gl_closed = 1
            where company_id = '%s' and acc_year = %d and acc_month = %d";
    $rc = $db->execute($sql, $companyId, $accYear, $accMonth);
    if ($rc === false) {
      return $this->sqlError(__METHOD__, __LINE__);
    }

    // 业务日志
    $log = "[$companyName] - 会计期间[{$p}]结转损益";
    $params["log"] = $log;

    // 操作成功
    return null;
  }
}
